/*
 * Created on 16 Jun 2006
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package com.aelitis.azureus.core.networkmanager;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;

import com.aelitis.azureus.core.networkmanager.Transport.ConnectListener;

public class ConnectionEndpoint {
    private InetSocketAddress notional_address;
    private ProtocolEndpoint[] protocols;

    public ConnectionEndpoint(InetSocketAddress _notional_address) {
        notional_address = _notional_address;
    }

    public InetSocketAddress getNotionalAddress() {
        return (notional_address);
    }

    public ProtocolEndpoint[] getProtocols() {
        if (protocols == null) {

            return (new ProtocolEndpoint[0]);
        }

        return (protocols);
    }

    public void addProtocol(ProtocolEndpoint ep) {
        if (protocols == null) {

            protocols = new ProtocolEndpoint[] { ep };

        } else {

            for (int i = 0; i < protocols.length; i++) {

                if (protocols[i] == ep) {

                    return;
                }
            }

            ProtocolEndpoint[] new_ep = new ProtocolEndpoint[protocols.length + 1];

            System.arraycopy(protocols, 0, new_ep, 0, protocols.length);

            new_ep[protocols.length] = ep;

            protocols = new_ep;
        }

        ep.setConnectionEndpoint(this);
    }

    public ConnectionEndpoint getLANAdjustedEndpoint() {
        ConnectionEndpoint result = new ConnectionEndpoint(notional_address);

        for (int i = 0; i < protocols.length; i++) {

            ProtocolEndpoint ep = protocols[i];

            InetSocketAddress address = ep.getAdjustedAddress(true);

            ProtocolEndpointFactory.createEndpoint(ep.getType(), result, address);
        }

        return (result);
    }

    public ConnectionAttempt connectOutbound(final boolean connect_with_crypto, final boolean allow_fallback, final byte[][] shared_secrets,
            final ByteBuffer initial_data, final int priority, final ConnectListener listener) {
        if (protocols.length == 1) {

            ProtocolEndpoint protocol = protocols[0];

            final Transport transport =
                    protocol.connectOutbound(connect_with_crypto, allow_fallback, shared_secrets, initial_data, priority, listener);

            return (new ConnectionAttempt() {
                public void abandon() {
                    if (transport != null) {

                        transport.close("Connection attempt abandoned");
                    }
                }
            });

        } else {

            final boolean[] connected = { false };
            final boolean[] abandoned = { false };

            final List<Transport> transports = new ArrayList<Transport>(protocols.length);

            final ConnectListener listener_delegate = new ConnectListener() {
                // private long start_time;
                private int timeout = Integer.MIN_VALUE;
                private int fail_count;

                public int connectAttemptStarted(int default_connect_timeout) {
                    synchronized (connected) {

                        if (timeout == Integer.MIN_VALUE) {

                            // start_time = SystemTime.getCurrentTime();

                            timeout = listener.connectAttemptStarted(default_connect_timeout);
                        }

                        return (timeout);
                    }
                }

                public void connectSuccess(Transport transport, ByteBuffer remaining_initial_data) {
                    boolean disconnect;

                    synchronized (connected) {

                        disconnect = abandoned[0];

                        if (!disconnect) {

                            if (!connected[0]) {

                                connected[0] = true;

                                // System.out.println( "Connect took " + (SystemTime.getCurrentTime() - start_time ) + " for " +
                                // transport.getDescription());

                            } else {

                                disconnect = true;
                            }
                        }
                    }

                    if (disconnect) {

                        transport.close("Transparent not required");

                    } else {

                        listener.connectSuccess(transport, remaining_initial_data);
                    }
                }

                public void connectFailure(Throwable failure_msg) {
                    boolean inform;

                    synchronized (connected) {

                        fail_count++;

                        inform = fail_count == protocols.length;
                    }

                    if (inform) {

                        listener.connectFailure(failure_msg);
                    }
                }

                public Object getConnectionProperty(String property_name) {
                    return (listener.getConnectionProperty(property_name));
                }
            };

            boolean ok = true;

            if (protocols.length != 2) {

                ok = false;

            } else {
                ProtocolEndpoint p1 = protocols[0];
                ProtocolEndpoint p2 = protocols[1];

                if (p1.getType() == ProtocolEndpoint.PROTOCOL_TCP && p2.getType() == ProtocolEndpoint.PROTOCOL_UTP) {

                } else if (p2.getType() == ProtocolEndpoint.PROTOCOL_TCP && p1.getType() == ProtocolEndpoint.PROTOCOL_UTP) {

                    ProtocolEndpoint temp = p1;
                    p1 = p2;
                    p2 = temp;
                } else {

                    ok = false;
                }

                if (ok) {

                    // p2 is uTP, p1 is TCP

                    final ByteBuffer initial_data_copy;

                    if (initial_data != null) {

                        initial_data_copy = initial_data.duplicate();

                    } else {

                        initial_data_copy = null;
                    }

                    Transport transport =
                            p2.connectOutbound(connect_with_crypto, allow_fallback, shared_secrets, initial_data, priority, new ConnectListenerEx(
                                    listener_delegate));

                    transports.add(transport);

                    final ProtocolEndpoint tcp_ep = p1;

                    SimpleTimer.addEvent("delay:tcp:connect", SystemTime.getCurrentTime() + 750, false, new TimerEventPerformer() {
                        public void perform(TimerEvent event) {
                            synchronized (connected) {

                                if (connected[0] || abandoned[0]) {

                                    return;
                                }
                            }

                            Transport transport =
                                    tcp_ep.connectOutbound(connect_with_crypto, allow_fallback, shared_secrets, initial_data_copy, priority,
                                            new ConnectListenerEx(listener_delegate));

                            synchronized (connected) {

                                if (abandoned[0]) {

                                    transport.close("Connection attempt abandoned");

                                } else {

                                    transports.add(transport);
                                }
                            }
                        }
                    });
                }
            }

            if (!ok) {

                Debug.out("No supportified!");

                listener.connectFailure(new Exception("Not Supported"));
            }

            return (new ConnectionAttempt() {
                public void abandon() {
                    List<Transport> to_kill;

                    synchronized (connected) {

                        abandoned[0] = true;

                        to_kill = new ArrayList<Transport>(transports);
                    }

                    for (Transport transport : to_kill) {

                        transport.close("Connection attempt abandoned");
                    }
                }
            });
        }
    }

    public String getDescription() {
        String str = "[";

        for (int i = 0; i < protocols.length; i++) {

            str += (i == 0 ? "" : ",") + protocols[i].getDescription();
        }

        return (str + "]");
    }

    private static class ConnectListenerEx implements ConnectListener {
        private ConnectListener listener;

        private boolean ok;
        private boolean failed;

        private ConnectListenerEx(ConnectListener _listener) {
            listener = _listener;
        }

        public int connectAttemptStarted(int default_connect_timeout) {
            return (listener.connectAttemptStarted(default_connect_timeout));
        }

        public void connectSuccess(Transport transport, ByteBuffer remaining_initial_data) {
            synchronized (this) {

                if (ok || failed) {

                    if (ok) {

                        Debug.out("Double doo doo");
                    }

                    return;
                }

                ok = true;
            }

            listener.connectSuccess(transport, remaining_initial_data);
        }

        public void connectFailure(Throwable failure_msg) {
            synchronized (this) {

                if (ok || failed) {

                    return;
                }

                failed = true;
            }

            listener.connectFailure(failure_msg);
        }

        public Object getConnectionProperty(String property_name) {
            return (listener.getConnectionProperty(property_name));
        }
    }
}
